// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef LIB_FPROMISE_SCHEDULER_H_
#define LIB_FPROMISE_SCHEDULER_H_

#include <map>
#include <queue>
#include <utility>

#include "promise.h"

namespace fpromise {
namespace subtle {

// Keeps track of runnable and suspended tasks.
// This is a low-level building block for implementing executors.
// For a concrete implementation, see |fpromise::single_threaded_executor|.
//
// Instances of this object are not thread-safe.  Its client is responsible
// for providing all necessary synchronization.
class scheduler final {
 public:
  using task_queue = std::queue<pending_task>;
  using ref_count_type = uint32_t;

  scheduler();
  ~scheduler();

  // Adds a task to the runnable queue.
  //
  // Preconditions:
  // - |task| must be non-empty
  void schedule_task(pending_task task);

  // Obtains a new ticket with a ref-count of |initial_refs|.
  // The executor must eventually call |finalize_ticket()| to update the
  // state of the ticket.
  //
  // Preconditions:
  // - |initial_refs| must be at least 1
  suspended_task::ticket obtain_ticket(ref_count_type initial_refs = 1);

  // Updates a ticket after one run of a task's continuation according
  // to the state of the task after its run.  The executor must call this
  // method after calling |obtain_ticket()| to indicate the disposition of
  // the task for which the ticket was obtained.
  //
  // Passing an empty |task| indicates that the task has completed so it
  // does not need to be resumed.
  //
  // Passing a non-empty |task| indicates that the task returned a pending
  // result and may need to be suspended depending on the current state
  // of the ticket.
  // - If the ticket has already been resumed, moves |task| into the
  //   runnable queue.
  // - Otherwise, if the ticket still has a non-zero ref-count, moves |task|
  //   into the suspended task table.
  // - Otherwise, considers the task abandoned and the caller retains
  //   ownership of |task|.
  //
  // Preconditions:
  // - |task| must be non-null (may be empty)
  // - the ticket must not have already been finalized
  void finalize_ticket(suspended_task::ticket ticket, pending_task* task);

  // Increments the ticket's ref-count.
  //
  // Preconditions:
  // - the ticket's ref-count must be non-zero (positive)
  void duplicate_ticket(suspended_task::ticket ticket);

  // Decrements the ticket's ref-count.
  //
  // If the task's ref-count reaches 0 and has an associated task that
  // has not already been resumed, returns the associated task back
  // to the caller.
  // Otherwise, returns an empty task.
  //
  // Preconditions:
  // - the ticket's ref-count must be non-zero (positive)
  pending_task release_ticket(suspended_task::ticket ticket);

  // Resumes a task and decrements the ticket's ref-count.
  //
  // If the ticket has an associated task that has not already been resumed,
  // moves its associated task to the runnable queue and returns true.
  // Otherwise, returns false.
  //
  // Preconditions:
  // - the ticket's ref-count must be non-zero (positive)
  bool resume_task_with_ticket(suspended_task::ticket ticket);

  // Takes all tasks in the runnable queue.
  //
  // Preconditions:
  // - |tasks| must be non-null and empty
  void take_runnable_tasks(task_queue* tasks);

  // Takes all remaining tasks, regardless of whether they are runnable
  // or suspended.
  //
  // This operation is useful when shutting down an executor.
  //
  // Preconditions:
  // - |tasks| must be non-null and empty
  void take_all_tasks(task_queue* tasks);

  // Returns true if there are any runnable tasks.
  bool has_runnable_tasks() const { return !runnable_tasks_.empty(); }

  // Returns true if there are any suspended tasks that have yet to
  // be resumed.
  bool has_suspended_tasks() const { return suspended_task_count_ > 0; }

  // Returns true if there are any tickets that have yet to be finalized,
  // released, or resumed.
  bool has_outstanding_tickets() const { return !tickets_.empty(); }

  scheduler(const scheduler&) = delete;
  scheduler(scheduler&&) = delete;
  scheduler& operator=(const scheduler&) = delete;
  scheduler& operator=(scheduler&&) = delete;

 private:
  struct ticket_record {
    ticket_record(ref_count_type initial_refs) : ref_count(initial_refs), was_resumed(false) {}

    // The current reference count.
    ref_count_type ref_count;

    // True if the task has been resumed using |resume_task_with_ticket()|.
    bool was_resumed;

    // The task is initially empty when the ticket is obtained.
    // It is later set to non-empty if the task needs to be suspended when
    // the ticket is finalized.  It becomes empty again when the task
    // is moved into the runnable queue, released, or taken.
    pending_task task;
  };
  using ticket_map = std::map<suspended_task::ticket, ticket_record>;

  task_queue runnable_tasks_;
  ticket_map tickets_;
  uint64_t suspended_task_count_ = 0;
  suspended_task::ticket next_ticket_ = 1;
};

}  // namespace subtle
}  // namespace fpromise

#endif  // LIB_FPROMISE_SCHEDULER_H_
